在前一章節中,我們聊到如何透過撰寫純函式,來幫我們的函式進行優化。
然而在剛開始寫程式時,我其實很難將函式中一些重複性的概念抽離出來,於是會寫出一些「過於流程化」的程式碼,而這樣的習慣總是讓我寫出一些髒髒的程式碼,要碼不是在同一個函式中創造不需要的副作用,不然就是無法預期自己的程式碼會長得怎麼樣。
自從我開始使用純函式的概念優化程式碼後,這個壞習怪改了很多,程式碼不僅變得更加嚴謹,自己也越來越能將程式進行抽象化。
但問題來了,什麼是「抽象化」呢?
關於如何進行更好的軟體設計、邏輯拆分,在電腦科學中有個理論可以更好協助理解 FP 究竟是怎麼協助我們進行程式碼的管理,這個理論是:抽象化(Abstraction)。
根據維基百科:「在電腦科學中,抽象化(英語:Abstraction)是將資料與程式,以它的語意來呈現出它的外觀,但是隱藏起它的實作細節。抽象化是用來減少程式的複雜度,使得程式設計師可以專注在處理少數重要的部份。」
而 Eric Elliott 在《Composing Software》一書中,針對抽象化提供了非常詳盡的解釋:
抽象化的手段方法大致上可以分成兩種,廣義化(Generalization)與特殊化(Specialization)。
廣義化指的是在重複的程式碼中,找到這些程式碼的共同點,並且隱藏這些明顯重複的實作內容;而特殊化指的是只針對程式碼中的異處進行處理。
舉例來說純函式處理的就是廣義化的部分,至於特殊化的應用,我們在後續的柯里化章節中再進行更深入的了解。
在軟體中,抽象化可以有很多種形式:
當然,我們的主題是 FP ,抽象化當然也能套用在函式上。
以往初學程式時,我們可能都有聽過以下範例:
在物件導向設計中,一台車可能會有輪子元件、引擎元件、加速的方法、煞車的方法,我們會去定義這些屬性的概念,就好像一個製作車子的模型,只是這些模型不是一體成型的,而是由不同的小模型組合而成,當我們需要時,就可以依照這些模型自定義出我們想要的車子模型。
上述的過程同時物件導向設計中常稱的介面(interface)概念,雖然介面的實作會比我上述的概念更複雜一點,但總結來說定義介面就是一種「抽象化」概念,我們把一些可套用在其他車輛的元素封裝成一個元件,之後如果要新增一台車物件,我們就可以讓這台車去繼承或是複製這個「抽象化的模型」,降低需要製造新車的成本。
只不過在 FP 中我們是以函式作為抽象化的單位,而不像物件導向會以元件作為單位,那在撰寫純函式時,我們可以怎麼進行抽象化呢?
相較於物件導向設計,純函式雖然不會有「自己的狀態」,也不會有那些很具體可見的屬性能做抽象化,例如:我們既不會實體化元件,所以也比較難透過元件的概念還具象化我們的函式,這也是自己在初學 FP 時最初遇到的障礙。
雖然 FP 看起來並不如傳統的 UI 元件直覺,但我們依然可以在封裝函式時思考,有哪些「主體」或是「細節」是不會影響函式本身目的,所以可以被抽離出來的?
舉例來說,如果我們要設計一個函式是用來「榨出蘋果汁」,所以最一開始我們會從採收蘋果開始,到將蘋果切片,丟進榨汁機,然後要打幾秒,最後才能產生出一杯 600 c.c. 的蘋果汁。
讓我們來練習上述那段敘述的邏輯拆分:
函式細節 | 是否是函式本身要處理的問題? | 原因 |
---|---|---|
自己種蘋果? | ||
超市買蘋果? | x | 不論是哪裡來的蘋果,都應該要可以榨出汁 |
蘋果切片? | ||
蘋果磨塊? | x | 不論蘋果是否是切塊、切片、不切,都要能被榨出汁 |
丟進榨汁機 | v | 我們的目的是「榨出蘋果汁」,總不可能丟進電鍋裡吧? |
要打幾秒 | x | 取決於使用者要喝什麼口感的果汁 |
一杯 600 c.c. | x | 取決使用多少蘋果來榨汁 |
結果我們拆解函式邏輯拆一拆發現,其實我們要的其實是一個果汁機!甚至要將什麼水果榨成汁都不是最重要的事。
雖然這個範例看起來有點荒謬,但自己覺得如 Eric Elliott 所述:「抽象化就是一種拆解的過程」,當你越觀察自己程式碼的細節,就越知道哪些東西應該要抽象化掉,去掉細節、留下真正該解決的問題。
於是最後我們獲得了一個超級精簡的純函式:
const blender = (fruit, amount) => `${amount} c.c. ${fruit} juice`;
如果這個果汁機將水果打小於一分鐘,會獲得奶昔般的口感,我們讓使用者決定要喝果汁還是果昔:
const blender = (fruit, amount, smoothie = false) => {
if(smoothie) {
return `${amount} c.c. ${fruit} smoothie`;
}
return `${amount} c.c. ${fruit} juice`;
};
在這邊我們可以將「是否要喝果汁還是果昔」想成「果汁打幾秒後會出現的結果」,最後讓我來驗證這個抽象化完的 blender 函式是否有符合 Pure Funciton 的準則?
const appleSmoothie1 = blender('apple', 600, true); // '600 c.c. apple smoothie'
const appleSmoothie2 = blender('apple', 600, true); // '600 c.c. apple smoothie'
const appleSmoothie3 = blender('apple', 400, true); // '400 c.c. apple smoothie'
const appleJuice = blender('apple', 600); // '600 c.c. apple juice'
經過驗證我們可以知道:
blender
函式符合單輸入單輸出原則如此我們就透過了「抽象化」的手法,將原本可能很複雜的函式,優化成了純函式。
我認為抽象化是 FP 中最難的一環,但好在我們可以在撰寫完程式碼後,透過拆解及觀察是否有無重複的細節,或是過多非必要的細節,來替我們的函式進行減量及抽象化,當程式碼複雜到一定程度時,甚至還可以拆解成兩個、多個不同的純函式。
當然抽象化的概念不止能應用在優化純函式中,更會在後續的內容中重複出現。
看到這邊,我們把純函式的核心概念給看了個大概,接著我們就針對文章內一直不斷重複提到的「副作用」來進行介紹吧!那我們下個章節見。